Skip to content

AOT: count a file as compiled without types only when the build compiles it - #48379

Open
robobun wants to merge 1 commit into
mainfrom
robobun/bfb65ff4/aot-untyped-count-compiled-files
Open

robobun wants to merge 1 commit into
mainfrom
robobun/bfb65ff4/aot-untyped-count-compiled-files

Conversation

@robobun

@robobun robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • The last line of bun check --aot and bun build --compile --aot counts files that the build does not compile. A bun init project with web/tsconfig.json = {} and three files that nothing imports prints No type errors in 1 file, 3 files compiled without types.
  • count_files (src/aot/report/file_counts.rs:83) counted each entry of RoleDecision.demoted, compiled or not.

Fix

  • count_files asks files.is_compiled before it counts a demoted file, as its loop does. The list and the roles do not change.
  • Not counted: files that only import type, an import() that never runs, or no code reaches.
  • Verified: test/aot/summary-line.test.ts (5 new cases), 1 unit test, 3 IR files. All fail on main. Also ran the bun_aot unit and IR suites.
  • Self-reviewed: 9 concerns raised, 8 addressed. Not taken: a docs sentence on how to get the types back (separate change).

Background

  • A build has one type checker. It demotes a file with // @ts-nocheck, or whose tsconfig.json differs in allowJs, lib or eight other options. Its types become guesses.
  • A tsconfig.json without include names each file below it. The build reads those files and compiles only what the program imports.
  • Considered a filter in decide_roles, which makes the list. Queries::file_trust reads the demoted role of files that are not compiled.

Downsides

  • A program that takes only a type from a demoted file prints no such words. That type is still a guess.
  • Each demoted file costs 2 to 9 more instructions, and no allocation. CI reports +0.0 KB for each release binary.
Notes

Origin. An automated fuzz report in the team chat. There is no GitHub issue. Reproduced on main 2596ac5 with the canary build of that commit.

Who sees it. The tsconfig.json that bun init writes has "allowJs": true and no include. A folder whose own tsconfig.json does not extend it and does not set allowJs needs another program. This includes a package of a monorepo. Two packages of 20 files each print No type errors in 1 file, 40 files compiled without types. The emitted program is the same with and without those folders.

History. The filter dates from #45631, when the list held only // @ts-nocheck files. #47942 added the tsconfig.json cause to the list and did not touch the filter.

What main prints, and what this branch prints (bun check --aot --no-pretty --explain=statistics, debug builds of the same tree):

project SourceFilesDemoted closing line after
no folder tsconfig.json 0 -> 0 1 file
web/tsconfig.json with a DOM lib, nothing imported 3 -> 0 1 file
the same, {} 3 -> 0 1 file
the same, one file by import type 3 -> 0 1 file
the same, one file imported for a value 3 -> 1 2 files, 1 file compiled without types
2 packages x 20 files, nothing imported 40 -> 0 1 file
// @ts-nocheck: one value import, one import type 2 -> 1 4 files, 1 file compiled without types
a .ts file of the folder as { type: "text" } 1 -> 0 2 files
the same beside a value import 2 -> 1 3 files, 1 file compiled without types

No other line of --explain=statistics changes in these 9 projects (time lines left out).

What the words mean. With nothing of the folder imported, the --aot-out program is the same with and without web/tsconfig.json (0 of 2,584 bytes differ), and main prints 3 files compiled without types. With one file of a folder imported for a value, web/tsconfig.json = {} against {"extends":"../tsconfig.json"} changes 125 of 99,384 bytes of the program. So the words now show when compiled code lost its types, and only then.

The text rows. A file embedded as text is a data module. Its tree role is Inferred, so find_unverified_files never lists it. What main counts there is the copy of the same path that the tsconfig.json names and that nothing imports as code. When no tsconfig.json names the file, main prints the right line for a text import. So is_compiled alone decides, and the fix needs no test of the kind of file.

Why the list stays. find_unverified_files (src/sema/driver/compile/roles.rs) lists each file of the program whose types the build does not verify, compiled or not. decide_roles sets the role of each to Inferred. Queries::file_trust (src/sema/check/aot/trust.rs) reads that role for the file that declares a type. So a type from a file that is only read stays a guess. The new IR test files-that-are-not-compiled.ts has a CHECK line for flags=[IsExpected] on such a type. The line does not hold when the inner tsconfig.json makes the same program.

Docs. docs/compiler/aot.mdx said that the last line counts the files of such a folder. It now says that the line counts the TypeScript files that the build compiles there. A .js file of the folder is compiled and never counted: with web/a.ts and web/j.js both imported, the line says 3 files, 1 file compiled without types.

Measurements (without = main 2596ac5, with = this branch):

  • The filter runs once for each entry of RoleDecision.demoted: 0 rounds with nothing unverified, 3 for the repro, 40 for 2 x 20 files.
  • Release-profile object of the crate (cargo build -p bun_aot --lib --release: -O3, one codegen unit, no LTO), with llvm-size, llvm-nm, llvm-objdump: text 5,066,837 -> 5,066,805 bytes, data unchanged. count_files is inlined into Build::check_and_lower::{closure#0}: 5,301 -> 5,269 bytes, 1,221 -> 1,226 instructions.
  • List loop in that object: 9 -> 11 instructions for an entry that is not compiled, 9 -> 18 for a compiled one. With an empty list the path is 13 -> 12 instructions.
  • Per-file loop: the source does not change. In that object it is 57 -> 58 instructions for each file, because the register allocator moves one spill.
  • Instruction counter of the crate over 10,000 calls (test profile, 249 program files): with an empty list 9,998,275 to 9,998,385 without and 9,997,979 to 9,998,198 with, so no difference outside the noise. Three entries add 54 to 55 instructions without and 76 to 77 with.
  • Release binaries in CI (build 139360 against main build 138398, the binary size check): +0.0 KB on each of the 12 targets.
  • Allocator calls in one count_files call (gdb hit counts on __rust_alloc, __rust_alloc_zeroed, __rust_realloc, __rust_dealloc, debug builds): 3 alloc, 6 realloc, 3 dealloc for the application project and 3, 1, 3 for the runtime project, without and with.
  • --aot-out program, debug builds without and with: 0 bytes differ for 6 inputs. The // @ts-nocheck and the nested tsconfig.json variant of a type-only file still differ from the plain one in 3 bytes each, as on main.
  • Existing expectations: 0 of the 109 IR files that have a STATISTIC: SourceFilesDemoted line change, and 0 unit assertions.

Suites.

  • test/aot/summary-line.test.ts: 14 of 14 pass. The 5 new cases fail on the main release build and on a debug build of main.
  • bun_aot unit tests (cargo test -p bun_aot --lib): 3290 of 3290 pass. The new one fails without the source change (2 where 1 is asserted).
  • IR suite (cargo test -p bun_aot --test ir_tests): 2602 of 2603 files pass. lower/modules/names-that-can-only-be-paths/loads.cjs fails with and without the change on my machine, because it lists the node_modules of the checkout. Without the source change, the 2 new files fail (SourceFilesDemoted is 3 and is 2), and so does programs/modules/file-another-tsconfig-imports-nothing.ts with its 2 new STATISTIC lines (is 3).

Self-review. The first version of this branch tested the kind that the loop gave the file (SourceFilesInferred), to leave out a .ts file that is embedded as text. The review showed that such a file is never on the list, so the test decided nothing. The fix is now the one is_compiled test, and the unit test of that state is gone. The review also asked for the two STATISTIC lines in file-another-tsconfig-imports-nothing.ts, for the word "TypeScript" in the docs clause, and for the first two lines below. Not taken: a new docs sentence on how to make the build check such a folder again (make both tsconfig.json files agree on the ten options).

Not changed here.

  • The line names no file and no cause, and --explain attributes nothing to a file that is compiled without types. DemotionCause has one variant for // @ts-nocheck and for a tsconfig.json that differs.
  • No output says that types read from a file that is not compiled are guesses.
  • roles.rs:137: a type-only file of a package that names the aot condition, under its own tsconfig.json, is still demoted, because is_aot_source exists for compiled files only. This is a trust question and needs its own tests.
  • RoleDecision.demoted still lists files that are not compiled. No other reader counts them.
  • The docs do not say that // @ts-nocheck also gives the words.

[human-review] gate passed · iteration 0 · 7 files touched

fails on main (without fix)
ASAN without fix: 5 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/aot/summary-line.test.ts
bun test v1.4.4 (367d939d9)

test/aot/summary-line.test.ts:
(pass) typed percentage > a class that extends a class of the runtime [16336.96ms]
(pass) typed percentage > parsed data that reaches a type [17569.07ms]
(pass) typed percentage > parsed data cast to a type [18437.66ms]
(pass) typed percentage > parsed data checked and built anew: only its own properties are looked up [26662.95ms]
(pass) typed percentage > properties of parsed data [30687.83ms]
(pass) typed percentage > no property access [24742.61ms]
342 |       slot(async () => {
343 |         const closingLine = new RegExp(
344 |           ` No type errors in ${counts}, compiled [\\d,]+ lines? in [\\d,]+\\.\\d\\d s \\([\\d,]+ lines/s\\)$`,
345 |         );
346 |         const { lines } = await check(files);
347 |         expect(lines.filter(line => line.includes("No type errors"))).toEqual([expect.stringMatching(closingLine)]);
                                                                            ^
error: expect(received).toEqual(expected)
... (truncated)

release without fix: 14 FAILED
bun test v1.4.3-canary.1 (367d939d9)

test/aot/summary-line.test.ts:
199 | async function check(files: Files, ...flags: string[]) {
200 |   using dir = tempDirWithBunTypes("aot-summary-line", files);
201 |   const command = [bunExe(), "check", "--aot", "--no-pretty", ...flags, "main.ts"];
202 |   const { stdout, stderr, exitCode, signal } = await spawned(command, String(dir), { ...bunEnv, NO_COLOR: "1" });
203 |   const said = exitCode === 0 ? "" : stdout + stderr;
204 |   expect({ exitCode, signal, said }).toEqual({ exitCode: 0, signal: null, said: "" });
                                           ^
error: expect(received).toEqual(expected)

  {
-   "exitCode": 0,
-   "said": "",
+   "exitCode": 1,
+   "said": 
+ "error: Script not found "check"
+ "
+ ,
    "signal": null,
  }

- Expected  - 2
+ Received  + 5

      at check (/workspace/bun/test/aot/summary-line.test.ts:204:38)
      at async numbersOf (/workspace/bun/test/aot/summary-line.test.ts:210:37)
      at async <anonymous> (/workspace/bun/test/aot/summary-line.test.ts:234:31)
      at async slot (/workspace/bun/test/aot/harness/commands.ts:31:18)
199 | async function check(files: Files, ...flags: string[])
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/aot/summary-line.test.ts
bun test v1.4.4 (367d939d9)

test/aot/summary-line.test.ts:
(pass) typed percentage > parsed data that reaches a type [29569.20ms]
(pass) typed percentage > parsed data cast to a type [30022.91ms]
(pass) typed percentage > parsed data checked and built anew: only its own properties are looked up [33543.34ms]
(pass) typed percentage > a class that extends a class of the runtime [33994.36ms]
(pass) typed percentage > properties of parsed data [42657.78ms]
(pass) closing line counts the files that are compiled without their types > the program imports nothing of a folder whose tsconfig.json is empty, below one that allows JavaScript [7993.34ms]
(pass) closing line [16794.80ms]
(pass) typed percentage > no property access [20205.29ms]
(pass) closing line counts the files that are compiled without their types > the program imports one file of a folder with another lib, and the type of another [6023.13ms]
(pass) closing line counts the files that are compiled without their types > the program imports one file of 
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     4f70933165
  features     lto, baseline

20 deps, 447 codegen, 1236 objects in 2597ms

ninja: Entering directory `/workspace/bun/build/release'
[1/5] fetch rust-argon2
[rust-argon2] up to date
[2/5] fetch lolhtml
[lolhtml] up to date
[2/5] cargo plan → /workspace/bun/build/release/rust-target/plan.json
303 units: 205 lib, 32 custom-build (host), 28 run custom-build, 16 proc-macro (host), 17 lib (host), 4 run custom-build (host), 1 rlib
[4/5] fetch WebKit (prebuilt)
[WebKit] fetching https://github.com/oven-sh/WebKit/releases/download/autobuild-5a61071f79134bf41844e7ea31a8449147d85dd4/bun-webkit-linux-amd64-lto-aot.tar.gz
[WebKit] extracted to /root/.bun/build-cache/webkit-5a61071f79134bf4-lto-aot (26.9s)
[4/5] reconfigure
[1/7] fetch WebKit (prebuilt)
[WebKit] up to date
[1/1631] rustc bun_output_tags 
[2/1631] rustc unicode_ident 
[3/1631] rustc unicode_xid 
[4/1631] rustc fnv 
[5/1631] rustc heck 
[6/1631] rustc ident_case 
[7/1631] mkdir stamps
[8/1631] mkdir codegen
[9/1631] rustc
... (truncated)
diff hotspot
docs/compiler/aot.mdx                              |   2 +-
 src/aot/report/file_counts.rs                      |   9 +-
 src/aot/report/file_counts/tests.rs                |  37 ++++++-
 .../file-another-tsconfig-imports-nothing.ts       |   2 +
 .../inner-refuses/file-embedded-as-text.ts         |  26 +++++
 .../inner-refuses/files-that-are-not-compiled.ts   |  36 +++++++
 test/aot/summary-line.test.ts                      | 114 +++++++++++++++++++++
 7 files changed, 220 insertions(+), 6 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
docs/compiler/aot.mdx                                         0      0     27
src/aot/report/file_counts.rs                                 1      1     27
src/aot/report/file_counts/tests.rs                           3      1     28
…ograms/modules/file-another-tsconfig-imports-nothing.ts      0      0     28
…nearest-tsconfig/inner-refuses/file-embedded-as-text.ts      0      0     27
…t-tsconfig/inner-refuses/files-that-are-not-compiled.ts      0      2     27
test/aot/summary-line.test.ts                                 2      1     26

…les it

The last line of `bun check --aot` and of `bun build --compile --aot`
counted every file whose types the build does not verify. That took in
files that the build does not compile: a file that nothing imports as
code, a file that is imported for a type, and a file that only an
`import()` that never runs names.

`count_files` now asks `files.is_compiled` before it counts a demoted
file, as its loop does for every other count. The list of demoted files
and the roles that trust reads stay as they are.
@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:42 AM PT - Nov 9th, 2026

✅ @robobun, your commit 4f7093316515102737b12692341320485dcf599d passed in Build #139360! 🎉


🧪   To try this PR locally:

bunx bun-pr 48379

That installs a local version of the PR into your bun-48379 executable, so you can run:

bun-48379 --bun

@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The diff is green in CI.

  • Build 139360: all 184 jobs that have finished passed. No job failed. The four macOS x64 test jobs are the only ones left. They expired three times while they waited for an agent (16:05, 17:05 and 19:05 UTC), and the build shows as failed each time. Two of them run now.
  • test/aot/summary-line.test.ts ran and passed on the Debian x64 and ASAN lanes. The cargo-test step passed with the bun_aot unit tests (3290) and the IR tests.
  • Four tests failed once and passed on a retry: bun-serve-html.test.ts, napi.test.ts, test-fastutf8stream-destroy.js and filesink.test.ts. This diff does not touch what they test.
  • The binary size check reports +0.0 KB on each of the 12 targets.

How I reproduced it on main 2596ac5, with the canary build of that commit:

tsconfig.json          the file that `bun init` writes
main.ts                console.log(1);
web/tsconfig.json      {}
web/f0.ts f1.ts f2.ts  export const v0 = 0;   (nothing imports them)

$ bun check --aot --no-pretty ./main.ts
✓ No type errors in 1 file, 3 files compiled without types, compiled 1 line in 0.11 s (8 lines/s)

$ bun build --compile --aot ./main.ts --outfile ./app.bin
compile  ./app.bin  3 files compiled without types, compiled 1 line in 0.10 s (9 lines/s)

With this branch neither line has those words. With web/f0.ts imported for a value, both lines say 1 file compiled without types (main says 3).

@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The AOT report now counts demoted files only when they are compiled. New tests cover file counts for nested TypeScript configurations, different import types, and files without type checks. The documentation clarifies which TypeScript files contribute to the compiled-without-types count.

Changes

AOT file-count reporting

Layer / File(s) Summary
Demoted-file count logic
src/aot/report/file_counts.rs, src/aot/report/file_counts/tests.rs
The demoted-file count now excludes files that are not compiled. Unit tests verify the reported counts and file roles.
AOT count validation
test/aot/ir/lower/programs/modules/*, test/aot/ir/lower/projects/nearest-tsconfig/inner-refuses/*, test/aot/summary-line.test.ts, docs/compiler/aot.mdx
Lowering and summary-line tests cover nested configurations, value, type-only, and text imports, and files without type checks. The documentation specifies which TypeScript files contribute to the compiled-without-types count.

Possibly related PRs

  • oven-sh/bun#48248: Documents that AOT checks exclude files the build does not compile, a related file-count behavior.

Suggested reviewers: jarred-sumner

Merge Risk: Minimal · up to 4f709

The change limits the compiled-without-types count to files the build compiles, with focused reporting tests and documentation updates. No merge-blocking issue is established; merge after normal checks pass.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: counting demoted files only when the build compiles them.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description follows the template's two required sections, "What does this PR do?" and "How did you verify your code works?", and covers them in depth. It states the problem (the closing line count…
Full details: Description check

Explanation

The description follows the template's two required sections, "What does this PR do?" and "How did you verify your code works?", and covers them in depth. It states the problem (the closing line counts demoted files that the build does not compile), the fix (count_files checks files.is_compiled before counting a demoted file), and what is not changed (the demoted list and roles). Verification covers new test cases in summary-line.test.ts, a unit test, and three IR fixtures, with before/after results on main and the branch, plus the unit and IR suite results. The extra Notes section adds reproduction tables and measurements. Minor gaps: the fuzz-report origin has no linked issue, and the description does not state an explicit list of changed docs beyond a short mention, but these are non-critical.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

Biome (2.5.15)

test/aot/ir/lower/programs/modules/file-another-tsconfig-imports-nothing.ts

File contains syntax errors that prevent linting: Line 28: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 28: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 31: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 31: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 31: Expected a statement but instead found ', "files": []'.

test/aot/ir/lower/projects/nearest-tsconfig/inner-refuses/file-embedded-as-text.ts

File contains syntax errors that prevent linting: Line 17: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 17: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 20: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 20: Expected a semicolon or an implicit semicolon after a statement, but found none

test/aot/ir/lower/projects/nearest-tsconfig/inner-refuses/files-that-are-not-compiled.ts

File contains syntax errors that prevent linting: Line 21: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 21: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 24: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 24: Expected a semicolon or an implicit semicolon after a statement, but found none

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 2596ac5 and 4f70933.

Files selected for processing: 7

  • docs/compiler/aot.mdx
  • src/aot/report/file_counts.rs
  • src/aot/report/file_counts/tests.rs
  • test/aot/ir/lower/programs/modules/file-another-tsconfig-imports-nothing.ts
  • test/aot/ir/lower/projects/nearest-tsconfig/inner-refuses/file-embedded-as-text.ts
  • test/aot/ir/lower/projects/nearest-tsconfig/inner-refuses/files-that-are-not-compiled.ts
  • test/aot/summary-line.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

@robobun

robobun commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

This review needs no change. I checked the two points that it raises.

  • docs: say which left-out code bun check --aot and bun compile --check type check #48248 edits other sentences of docs/compiler/aot.mdx (lines 94 and 100). This PR edits line 109. git merge-tree of the two branches reports no conflict.
  • The three files that Biome could not parse are IR text tests. Each file holds a tree of files in // FILE: sections, and some sections are JSON (test/aot/ir/README.md). file-another-tsconfig-imports-nothing.ts has that shape on main too.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants